/* CsJni - .NET / Java Bridge
 * 
 * Copyright 2010 Robert E. Burke Licensed under the Apache License, Version 
 * 2.0 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
 * law or agreed to in writing, software distributed under the License is 
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using CsJni.PlatformAbstraction;

// See http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/jniTOC.html
// See http://java.sun.com/docs/books/jni/html/jniTOC.html

namespace CsJni
{
    [Flags]
    public enum JavaVMLogTypes
    {
        None = 0,
        RedirectVM = 1, // vfprintf
        RedirectStdOut = 2,
        RedirectStdErr = 4,
        VMWrapperLogging = 8,
    }

    public interface IJavaVMCallbackHandler
    {
        void HandleLogging(JavaVMLogTypes logType, string output);
    }

    public sealed partial class JavaVM
    {
        private static JavaVM mInstance; // TODO - review statics

        private static Dictionary<string, DelegatePin> mPinnedDelegates = new Dictionary<string, DelegatePin>();
        private static Object mLockingObject = new Object();
        private static bool mAllowReuseOfExistingVM;

        private JniEnvironmentCache mJniEnvironmentCache;
        private JavaVMLoader mLoader;
        private IJavaVMCallbackHandler mIJavaVMHookHandler;
        private JavaVMConfiguration mJvmConfiguration;
        private IntPtr mJavaVM;
        private JniInvokeInterface mInvokeInterface;
        private JavaNativeMethodRegistrar mNativeMethodRegistrar;

        private const CallingConvention PrivCC = CallingConvention.Winapi;

        public const CallingConvention CC = PrivCC;

        private JavaVM()
        {
        }

        public static bool AllowReuseOfExistingVM
        {
            get { return mAllowReuseOfExistingVM; }
            set { mAllowReuseOfExistingVM = value; }
        }

        public static bool IsInitialized
        {
            get { return (Instance != null); }
        }

        private static JavaVM Instance
        {
            get { return mInstance; } // if this is null, Create has not been  called yet
        }

        public static void Release()
        {
            if (Instance != null)
            {
                Instance.ReleaseCore(false);
            }
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void Initialize(JavaVMConfiguration configuration, IJavaVMCallbackHandler hookHandler)
        {
            StaticInitializeCore(configuration, hookHandler);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void Initialize(JavaVMConfiguration configuration)
        {
            StaticInitializeCore(configuration, null);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void Initialize(IJavaVMCallbackHandler hookHandler)
        {
            StaticInitializeCore(null, hookHandler);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void Initialize()
        {
            StaticInitializeCore(null, null);
        }

        public static JniVersion JniVersion
        {
            get { return Instance.JvmConfiguration.Version; }
        }

        public static JniEnvironment FindEnvironment()
        {
            lock (mLockingObject)
            {
                if (Instance.mJniEnvironmentCache == null)
                {
                    return null;
                }
                bool isNew;
                return Instance.mJniEnvironmentCache.GetEnvironment(out isNew);
            }
        }

        public static JniEnvironment EnvironmentFromHandle(IntPtr handle)
        {
            if (Instance.mJniEnvironmentCache == null)
            {
                return null;
            }
            return Instance.mJniEnvironmentCache.FromHandle(handle);
        }

        public static JniThreadContext FindThreadContext()
        {
            if (Instance.mJniEnvironmentCache == null)
            {
                return null;
            }
            bool isNew;
            JniEnvironment environment = Instance.mJniEnvironmentCache.GetEnvironment(out isNew);
            return new JniThreadContext(environment, isNew);
        }

        internal static JniEnvironment SafeFindEnvironment()
        {
            lock (typeof(JavaVM))
            {
                JavaVM javaVM = Instance;
                if (javaVM != null)
                {
                    return FindEnvironment();
                }
            }
            return null;
        }

        internal static void HandleEnvironmentFinalization(JniEnvironment environment)
        {
            if (Instance != null && Instance.mJniEnvironmentCache != null)
            {
                Instance.mJniEnvironmentCache.HandleFinalization(environment);
            }
        }

        private void DestroyJavaVM()
        {
            int result = mInvokeInterface.DestroyJavaVM(this.mJavaVM);
            if (0 != result)
            {
                JniException.CheckVMErrorCode(result);
            }
        }

        internal static IntPtr GetEnvironment()
        {
            unsafe
            {
                void** ppEnv = null;
                int result = Instance.mInvokeInterface.GetEnv(Instance.mJavaVM, &ppEnv, (int)Instance.JvmConfiguration.Version);
                if (result == (int)JniErrorCode.Detached)
                {
                    return IntPtr.Zero;
                }
                if (0 != result)
                {
                    JniException.CheckVMErrorCode(result);
                }
                IntPtr environmentHandle = new IntPtr((void*)ppEnv);
                return environmentHandle;
            }
        }

        internal static IntPtr AttachCurrentThread(JavaVMThreadAttachArguments arguments)
        {
            JavaVMAttachArgs attachArgs = new JavaVMAttachArgs();
            if (arguments != null)
            {
                attachArgs = arguments.ToArgs(Instance.JvmConfiguration.Version);
            }
            try
            {
                unsafe
                {
                    void** ppEnv = null;
                    void* pArgs = null;
                    if (arguments != null)
                    {
                        pArgs = &attachArgs;
                    }
                    int result = Instance.mInvokeInterface.AttachCurrentThread(Instance.mJavaVM, &ppEnv, pArgs);
                    if (0 != result)
                    {
                        JniException.CheckVMErrorCode(result);
                    }
                    IntPtr environmentHandle = new IntPtr((void*)ppEnv);
                    return environmentHandle;
                }
            }
            finally
            {
                if (arguments != null)
                {
                    JavaVMThreadAttachArguments.FreeArgs(attachArgs);
                }
            }
        }

        internal static void DetachCurrentThread()
        {
            int result = Instance.mInvokeInterface.DetachCurrentThread(Instance.mJavaVM);
            if (0 != result)
            {
                JniException.CheckVMErrorCode(result);
            }
        }

        internal static void AddDelegatePins(DelegatePin[] pins)
        {
            foreach (DelegatePin pin in pins)
            {
                AddDelegatePin(pin);
            }
        }

        internal static void AddDelegatePin(DelegatePin pin)
        {
            lock (mPinnedDelegates)
            {
                mPinnedDelegates[pin.Name] = pin;
            }
        }

        private static void StaticInitializeCore(JavaVMConfiguration configuration, IJavaVMCallbackHandler hookHandler)
        {
            if (Instance != null)
            {
                throw new JniException(JniResourceManager.Instance.GetString("jvmCoreInitOnlyOnce"));
            }
            mInstance = new JavaVM();
            Instance.InitializeCore(configuration, hookHandler);
        }

        private void InitializeCore(JavaVMConfiguration configuration, IJavaVMCallbackHandler hookHandler)
        {
            try
            {
                Log("32/64-bit: " + (IntPtr.Size * 8));

                AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
                AppDomain.CurrentDomain.DomainUnload += new EventHandler(CurrentDomain_DomainUnload);

                mIJavaVMHookHandler = hookHandler;

                mJvmConfiguration = InitializeConfiguration(configuration);
                PlatformFactory.Create(mJvmConfiguration);
                mJvmConfiguration.CanonicalizePaths(PlatformFactory.Platform);

                mLoader = new JavaVMLoader(this.JvmConfiguration);

                InitializeHooks();

                IntPtr loadedVM = mLoader.GetFirstCreatedVM();
                IntPtr environmentHandle = IntPtr.Zero;
                if (loadedVM != IntPtr.Zero)
                {
                    if (!AllowReuseOfExistingVM)
                    {
                        throw new JniException("VM already exists.");
                    }
                    if (IsLoggingEnabled)
                    {
                        Log("Using existing VM.");
                    }
                    mJavaVM = loadedVM;
                }
                else
                {
                    mJavaVM = mLoader.Load(out environmentHandle);
                }
                mInvokeInterface = new JniInvokeInterface(this.mJavaVM, this.JvmConfiguration.Version);

                if (loadedVM != IntPtr.Zero)
                {
                    environmentHandle = JniEnvironmentCache.GetEnvironmentHandle();
                }

                mJniEnvironmentCache = new JniEnvironmentCache(environmentHandle);
                JniEnvironment environment = FindEnvironment();

                mNativeMethodRegistrar = new JavaNativeMethodRegistrar(mJvmConfiguration, environment);
                JavaStdStreamRedirector.Init(environment);

                if (IsLoggingEnabled)
                {
                    JavaSystemPropertiesHelper.LogBannerProperties(environment);
                }
            }
            catch (Exception ex)
            {
                throw new JniException(JniResourceManager.Instance.GetString("jvmCoreFailedToCreate"), ex);
            }
        }

        // TODO - only needs to be visible by proxy generator
        public static Dictionary<string, string> RetrieveAllJavaProperties(JniEnvironment environment)
        {
            return JavaSystemPropertiesHelper.RetrieveAll(environment);
        }

        internal static bool IsLoggingEnabled
        {
            get
            {
                return IsLogTypeEnabled(JavaVMLogTypes.VMWrapperLogging);
            }
        }

        internal static void Log(string message)
        {
            HandleLogging(JavaVMLogTypes.VMWrapperLogging, message);
        }

        internal static bool IsLogTypeEnabled(JavaVMLogTypes logType)
        {
            return (Instance != null && Instance.mIJavaVMHookHandler != null &&
                ((Instance.mJvmConfiguration.JavaVMLogTypes & logType) != 0));
        }

        internal static void LogMemory(bool triedToRelease, string message)
        {
            if (triedToRelease)
            {
                System.Diagnostics.Trace.WriteLine(message); // TODO
            }
            else
            {
                if (!Environment.HasShutdownStarted)
                {
                    throw new NotImplementedException();
                    // TODO
                }
            }
        }

        void CurrentDomain_DomainUnload(object sender, EventArgs e)
        {
            ReleaseCore(true);
        }

        void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            ReleaseCore(true);
        }

        private void ReleaseCore(bool appDomainUnloading)
        {
            lock (mLockingObject)
            {
                if (Instance == null)
                {
                    return;
                }
                GC.WaitForPendingFinalizers();
                if (appDomainUnloading)
                {
                    AppDomain.CurrentDomain.ProcessExit -= new EventHandler(CurrentDomain_ProcessExit);
                    AppDomain.CurrentDomain.DomainUnload -= new EventHandler(CurrentDomain_DomainUnload);
                }
                if (appDomainUnloading && mPinnedDelegates != null)
                {
                    foreach (DelegatePin pin in mPinnedDelegates.Values)
                    {
                        ((IDisposable)pin).Dispose();
                    }
                    mPinnedDelegates.Clear();
                    mPinnedDelegates = null;
                }
                mNativeMethodRegistrar = null;
                try
                {
                    if (appDomainUnloading)
                    {
                        DetachCurrentThread();
                    }
                    if (appDomainUnloading && !AllowReuseOfExistingVM && this.mJavaVM != IntPtr.Zero)
                    {
                        //DestroyJavaVM();
                    }
                }
                catch (JniException)
                {
                }
                finally
                {
                    mJavaVM = IntPtr.Zero;
                }
                JniEnvironment.Release();
                mInstance = null;
                PlatformFactory.Platform.Release();
            }
        }

        private static JavaVMConfiguration InitializeConfiguration(JavaVMConfiguration configuration)
        {
            if (configuration == null)
            {
                configuration = JavaVMConfiguration.FromAppConfiguration();
                if (configuration == null)
                {
                    configuration = JavaVMConfiguration.CreateDefault(); // use all defaults
                }
            }
            return configuration;
        }

        private JavaVMConfiguration JvmConfiguration
        {
            get { return mJvmConfiguration; }
        }

        private void InitializeHooks()
        {
            this.JvmConfiguration.AddReservedOption("exit",
               CreateVMHook(new Jni_exit_Handler(this.Jni_exit), "exit"));
            this.JvmConfiguration.AddReservedOption("abort",
                CreateVMHook(new Jni_abort_Handler(this.Jni_abort), "abort"));

            if (IsLogTypeEnabled(JavaVMLogTypes.RedirectVM))
            {
                bool ok = true;
                try
                {
                    PlatformFactory.Platform.FaultVfPrintf();
                }
                catch (JniException ex)
                {
                    Log("Unable to hook vfprintf: " + ex.ToString());
                    ok = false;
                }
                if (ok)
                {
                    this.JvmConfiguration.AddReservedOption("vfprintf",
                        CreateVMHook(new Jni_vfprintf_Handler(this.Jni_vfprintf), "vfprintf"));
                }
            }
        }

        private static IntPtr CreateVMHook(Delegate theDelegate, string name)
        {
            DelegatePin pin = new DelegatePin(theDelegate, name);
            AddDelegatePin(pin);
            return pin.ToFunctionPointer();
        }

        private static bool IsJavaDriving
        {
            get { return false; } // TODO
        }

        private delegate void Jni_exit_Handler(int status);

        private void Jni_exit(int status)
        {
            if (IsJavaDriving)
            {
                Log("JVM exited with code: " + status + ", exiting CLR.");
                Environment.Exit(status);
            }
            else
            {
                Log("JVM exited with code: " + status);
            }
        }

        private delegate void Jni_abort_Handler();

        private void Jni_abort()
        {
            if (IsJavaDriving)
            {
                Log("JVM terminated abnormally, exiting CLR (255).");
                Environment.Exit(255);
            }
            else
            {
                Log("JVM terminated abnormally.");
            }
        }

        // jint (JNICALL *vfprintf)(FILE *fp, const char *format, va_list arguments);
        private delegate int Jni_vfprintf_Handler(IntPtr fp, IntPtr format, IntPtr arguments);

        private int Jni_vfprintf(IntPtr filePointer, IntPtr format, IntPtr arguments)
        {
            if (!IsLogTypeEnabled(JavaVMLogTypes.RedirectVM))
            {
                return 0;
            }
            string output = PlatformFactory.Platform.ExtractVfprintf(filePointer, format, arguments);
            mIJavaVMHookHandler.HandleLogging(JavaVMLogTypes.RedirectVM, output);
            return output.Length;
        }

        internal static void HandleLogging(JavaVMLogTypes logType, string message)
        {
            if (!IsLogTypeEnabled(logType))
            {
                return;
            }
            Instance.mIJavaVMHookHandler.HandleLogging(logType, message);
        }

        internal static JavaNativeMethodRegistrar JavaNativeMethodRegistrar
        {
            get // TODO - thread safety
            {
                return (Instance != null) ? Instance.mNativeMethodRegistrar : null;
            }
        }
    }
}
